﻿

using JESAI.DistributedLock.Internal;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace JESAI.DistributedLock.FileSystem;

/// <summary>
/// 一种基于持有锁文件独占句柄的分布式锁。解锁后，该文件将被删除。
/// </summary>
public sealed partial class FileDistributedLock : IInternalDistributedLock<FileDistributedLockHandle>
{
    /// <summary>
    /// 由于<see cref="UnauthorizedAccessException"/>可以暂时抛出，也可以针对权限问题抛出，因此我们在假设问题不是暂时的之前，会重试多次。根据经验，我发现这个值在本地和AppVeyor上都是可靠的（如果有问题，尝试更多次几乎没有风险，因为我们最终会失败）。
    /// </summary>
    private const int MaxUnauthorizedAccessExceptionRetries = 1600;

    // 这些目前是不可配置的，因为将来我们可能希望更改FileDistributedLock的实现，以利用可能允许实际阻塞的本机方法。这里的值反映了我们希望在很少发生争用的情况下使用文件锁的想法
    private static readonly TimeoutValue MinBusyWaitSleepTime = TimeSpan.FromMilliseconds(50),
        MaxBusyWaitSleepTime = TimeSpan.FromSeconds(1);

    private string? _cachedDirectory;

    /// <summary>
    /// 构造一个锁，该锁使用提供的<paramref name="lockFile"/>作为确切的文件名。
    /// 
    /// 获取锁后，如果文件的目录不存在，则会自动创建该目录。如果文件不存在，也将类似地创建该文件，并在释放锁时将其删除。
    /// </summary>
    public FileDistributedLock(FileInfo lockFile)
    {
        this.Name = (lockFile ?? throw new ArgumentNullException(nameof(lockFile))).FullName;
        if (lockFile.Name.Length == 0) { throw new FormatException($"{nameof(lockFile)}: may not have an empty file name"); }
    }

    /// <summary>
    /// 构造一个锁，将锁文件放置在<paramref name="lockFileDirectory"/>中。文件名将基于<paramref name="name"/>，但会使用适当的转义/哈希来确保生成有效的文件名。
    /// 
    /// 获取锁后，如果文件的目录不存在，则会自动创建该目录。如果文件不存在，也将类似地创建该文件，并在释放锁时将其删除。
    /// </summary>
    public FileDistributedLock(DirectoryInfo lockFileDirectory, string name)
    {
        this.Name = FileNameValidationHelper.GetLockFileName(lockFileDirectory, name);
    }

    /// <summary>
    /// 实现 <see cref="IDistributedLock.Name"/>
    /// </summary>
    public string Name { get; }

    private string Directory => this._cachedDirectory ??= Path.GetDirectoryName(this.Name);

    ValueTask<FileDistributedLockHandle?> IInternalDistributedLock<FileDistributedLockHandle>.InternalTryAcquireAsync(TimeoutValue timeout, CancellationToken cancellationToken) =>
        BusyWaitHelper.WaitAsync(
            state: this,
            tryGetValue: (@this, token) => @this.TryAcquire(token).AsValueTask(),
            timeout: timeout,
            minSleepTime: MinBusyWaitSleepTime,
            maxSleepTime: MaxBusyWaitSleepTime,
            cancellationToken
        );

    private FileDistributedLockHandle? TryAcquire(CancellationToken cancellationToken)
    {
        var retryCount = 0;

        while (true)
        {
            cancellationToken.ThrowIfCancellationRequested();

            this.EnsureDirectoryExists();

            FileStream lockFileStream;
            try
            {
                // 关键论点：
                // OpenOrCreate对文件是否存在具有鲁棒性
                // 没有人可以拿走独占锁
                // DeleteOnClose来清理我们自己
                lockFileStream = new FileStream(this.Name, FileMode.OpenOrCreate, FileAccess.Read, FileShare.None, bufferSize: 1, FileOptions.DeleteOnClose);
            }
            catch (DirectoryNotFoundException)
            {
                // 这几乎不应该发生，因为我们刚刚创建了目录，但在竞争条件下它可能会发生。请重试
                continue;
            }
            catch (UnauthorizedAccessException)
            {
                // 这可能发生在少数情况下：

                // 路径已经是目录，所以我们永远无法将其作为文件打开
                if (System.IO.Directory.Exists(this.Name))
                {
                    throw new InvalidOperationException($"Failed to create lock file '{this.Name}' because it is already the name of a directory");
                }

                // 该文件存在并且是只读的
                FileAttributes attributes;
                try { attributes = File.GetAttributes(this.Name); }
                catch { attributes = FileAttributes.Normal; } // 例如，可能会因FileNotFoundException而失败
                if (attributes.HasFlag(FileAttributes.ReadOnly))
                {
                    // 一旦我们检测到文件是只读的，或者我们现在只抛出一个用例，我们可以通过避免DeleteOnClose来支持这一点
                    throw new NotSupportedException($"Locking on read-only file '{this.Name}' is not supported");
                }

                // 令人沮丧的是，由于并发创建/删除，此错误可能会暂时抛出。最初假设它是暂时的，然后重试
                if (CanRetryTransientFileSystemError(ref retryCount))
                {
                    continue;
                }

                // 如果我们到了这里，我们已经用尽了重试：假设这是一个合法的权限问题
                throw;
            }
            // 这不应该发生，因为我们验证了。但是，如果确实如此（例如，由于某些系统配置更改？），请抛出，这样它就不会出现在IOException块中（PathTooLongException是IOException）
            catch (PathTooLongException) { throw; }
            catch (IOException)
            {
                // 希望如果我们到达这里，唯一的失败原因是文件被锁定
                return null;
            }

            return new FileDistributedLockHandle(lockFileStream);
        }
    }

    private void EnsureDirectoryExists()
    {
        var retryCount = 0;

        while (true)
        {
            try
            {
                System.IO.Directory.CreateDirectory(this.Directory);
                return;
            }
            catch (Exception ex)
            {
                // 这可能表示并发创建/删除过程中出现暂时性故障或权限问题。
                // 如果我们遇到它，假设它是暂时的，除非它持续存在。
                // 很长一段时间以来，我只是在这里检查了Unauthorized AccessException。然而，最近在Linux上的测试表明，在竞争条件下，我们也可以看到IOException，这可能是因为在目录创建过程中有一段时间它以文件的形式出现。
                if (ex is UnauthorizedAccessException or IOException
                    && CanRetryTransientFileSystemError(ref retryCount))
                {
                    continue;
                }

                throw new InvalidOperationException($"Failed to ensure that lock file directory {this.Directory} exists", ex);
            }
        }
    }

    private static bool CanRetryTransientFileSystemError(ref int retryCount)
    {
        if (retryCount >= MaxUnauthorizedAccessExceptionRetries) { return false; }

        ++retryCount;

        return true;
    }
}
